Skip to content

Claude/route logging diagnostics#22

Merged
jgeudens merged 13 commits intomasterfrom
claude/route-logging-diagnostics-4Awjy
Apr 5, 2026
Merged

Claude/route logging diagnostics#22
jgeudens merged 13 commits intomasterfrom
claude/route-logging-diagnostics-4Awjy

Conversation

@jgeudens
Copy link
Copy Markdown
Member

@jgeudens jgeudens commented Apr 3, 2026

Summary by CodeRabbit

  • New Features

    • Adapter emits diagnostic notifications (level + message) and these are forwarded into application logs with level→severity mapping.
  • Configuration

    • Handshake timeout is now configurable; Modbus adapter defaults changed: connection persistence enabled, device int32 set little‑endian.
  • Documentation

    • JSON‑RPC spec documents adapter.diagnostic emission and adds an "error" level.
  • Bug Fixes

    • Session shutdown is asynchronous: sessionStopped emitted only after the adapter process exits.
  • Tests

    • New tests cover diagnostics, level mapping, malformed params and shutdown/stop timing.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

AdapterClient gained a handshakeTimeoutMs parameter, emits diagnosticReceived(level,message) for JSON‑RPC adapter.diagnostic notifications, and defers sessionStopped until the adapter process exits. AdapterProcess::stop() is non‑blocking with a kill timer. ModbusPoll subscribes to and logs adapter diagnostics. Tests, docs and logging category updated.

Changes

Cohort / File(s) Summary
Adapter client
src/ProtocolAdapter/adapterclient.h, src/ProtocolAdapter/adapterclient.cpp
Constructor gains handshakeTimeoutMs (stored in _handshakeTimeoutMs); added slot onNotificationReceived(QString, QJsonValue) to filter adapter.diagnostic, validate object params and emit diagnosticReceived(QString,QString); handshake timeout/error paths and session stop/state transitions adjusted to defer sessionStopped until process exit and suppress sessionError during stopping.
Adapter process stop behaviour
src/ProtocolAdapter/adapterprocess.h, src/ProtocolAdapter/adapterprocess.cpp
stop() now closes stdin and returns immediately, using a _pKillTimer/QTimer to forcefully kill the process after cStopTimeoutMs if still running; kill timer reset on start/finish; docs updated to reflect asynchronous stop.
ModbusPoll logging
src/communication/modbuspoll.h, src/communication/modbuspoll.cpp
Connected to AdapterClient::diagnosticReceived; added onAdapterDiagnostic(level,message) mapping diagnostic levels (debug,info,warning,error) to Qt logging severities and emitting a warning for unknown levels; adjusted session error/stop reconnection wiring.
Logging category
src/util/scopelogging.h, src/util/scopelogging.cpp
Renamed logging category from scope.comm.connection/scopeCommConnection to scope.comm.adapter/scopeAdapter and updated declarations/usages.
Adapter defaults & docs
adapters/describe.json, adapters/json-rpc-spec.md
Defaults changed: connections[0].persistent: falsetrue, devices[0].int32LittleEndian: falsetrue; adapter.diagnostic documented as actively emitted for Qt log messages and "error" level added to mapping.
Tests — AdapterClient
tests/ProtocolAdapter/tst_adapterclient.h, tests/ProtocolAdapter/tst_adapterclient.cpp
Added MockAdapterProcess::injectProcessFinished(); tests for diagnostic forwarding, debug level and malformed params; added shutdown/shutdown-ack/sessionStopped timing tests and process‑error‑during‑stopping tests verifying sessionStopped/sessionError semantics.
Tests — ModbusPoll
tests/communication/tst_modbuspoll.h, tests/communication/tst_modbuspoll.cpp, tests/communication/CMakeLists.txt, tests/CMakeLists.txt
Added TestModbusPoll fixture and tests verifying level→Qt log mapping; added tst_modbuspoll test target and included communication tests in overall test CMake.
Diagnostic model & tests
src/models/diagnosticmodel.cpp, tests/models/tst_diagnosticmodel.h, tests/models/tst_diagnosticmodel.cpp
Minor signature formatting changes; clear() now guards against empty removal range; added clearEmpty() unit test.
AdapterProcess tests timing
tests/ProtocolAdapter/tst_adapterprocess.cpp
Inserted short QTest::qWait(500) waits after process.stop() in two tests to allow asynchronous stop/exit handling.

Sequence Diagram(s)

sequenceDiagram
    participant AdapterProc as AdapterProcess
    participant AdapterClient as AdapterClient
    participant ModbusPoll as ModbusPoll
    participant QtLog as Qt Logging

    AdapterProc->>AdapterClient: JSON-RPC notification\n(method: "adapter.diagnostic", params:{level,message})
    activate AdapterClient
    AdapterClient->>AdapterClient: onNotificationReceived()\nif method=="adapter.diagnostic"
    AdapterClient->>AdapterClient: validate params is object\nextract level, message
    AdapterClient-->>ModbusPoll: emit diagnosticReceived(level, message)
    deactivate AdapterClient

    activate ModbusPoll
    ModbusPoll->>ModbusPoll: onAdapterDiagnostic()\nmap level -> Qt log severity
    ModbusPoll-->>QtLog: log message at mapped level
    deactivate ModbusPoll
Loading

Possibly related PRs

  • Remove modbus protocol and related code #3: Modifies src/communication/modbuspoll.*; likely to conflict with the added diagnostic handling and ModbusPoll changes.
  • Support adapter #5: Touches AdapterClient and ModbusPoll integration and lifecycle; strongly related to the handshake/diagnostic and stop semantics changes.
  • Refactor modbus register #18: Also alters ModbusPoll internals (registers/signatures) and may conflict with the new diagnostic slot and test additions.
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Claude/route logging diagnostics' is vague and uses non-descriptive terms that don't clearly convey the main purpose of the changeset. Revise the title to clearly describe the primary change, e.g., 'Add diagnostic logging routing from adapter to ModbusPoll' or 'Implement adapter diagnostic message forwarding and improved shutdown handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/route-logging-diagnostics-4Awjy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/communication/tst_modbuspoll.cpp (1)

9-19: Consider resetting captured values at the start of each test.

If a test runs and the target method fails to emit a log message (e.g., due to a regression), the test could incorrectly pass using stale values from a previous test. Resetting the globals before each invocation improves test isolation.

♻️ Proposed fix
     void captureHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
     {
         g_capturedType = type;
         g_capturedMessage = msg;
     }
+
+    void resetCaptured()
+    {
+        g_capturedType = QtMsgType{};
+        g_capturedMessage.clear();
+    }
 }

Then call resetCaptured() at the start of each test, e.g.:

void TestModbusPoll::diagnosticDebugLevel()
{
    resetCaptured();
    QtMessageHandler previous = qInstallMessageHandler(captureHandler);
    // ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/communication/tst_modbuspoll.cpp` around lines 9 - 19, Add a
resetCaptured() helper that sets g_capturedType to a default and clears
g_capturedMessage, and call resetCaptured() at the start of each test that uses
captureHandler (e.g., TestModbusPoll::diagnosticDebugLevel and other
TestModbusPoll tests) so stale global values cannot make a test pass; update
tests to invoke resetCaptured() before qInstallMessageHandler(captureHandler)
and before exercising the code under test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 170-178: The diagnostic handler should validate that params
contains string fields "level" and "message" before emitting; update the code in
adapterclient.cpp where params and QJsonObject obj are used to check
obj.contains("level") && obj.value("level").isString() and similarly for
"message", and if either check fails call qCWarning(scopeComm) with a clear
message (including the raw params) and return without emitting
diagnosticReceived; otherwise call diagnosticReceived with the verified string
values (use toString() only after isString()).

---

Nitpick comments:
In `@tests/communication/tst_modbuspoll.cpp`:
- Around line 9-19: Add a resetCaptured() helper that sets g_capturedType to a
default and clears g_capturedMessage, and call resetCaptured() at the start of
each test that uses captureHandler (e.g., TestModbusPoll::diagnosticDebugLevel
and other TestModbusPoll tests) so stale global values cannot make a test pass;
update tests to invoke resetCaptured() before
qInstallMessageHandler(captureHandler) and before exercising the code under
test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: afce44c4-9607-4aae-9de5-47d21cba23d6

📥 Commits

Reviewing files that changed from the base of the PR and between 180a3bd and c91f778.

📒 Files selected for processing (10)
  • src/ProtocolAdapter/adapterclient.cpp
  • src/ProtocolAdapter/adapterclient.h
  • src/communication/modbuspoll.cpp
  • src/communication/modbuspoll.h
  • tests/CMakeLists.txt
  • tests/ProtocolAdapter/tst_adapterclient.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h
  • tests/communication/CMakeLists.txt
  • tests/communication/tst_modbuspoll.cpp
  • tests/communication/tst_modbuspoll.h

claude and others added 4 commits April 3, 2026 17:21
AdapterClient now connects to AdapterProcess::notificationReceived and
emits diagnosticReceived(level, message) for adapter.diagnostic
notifications. ModbusPoll connects to this signal and routes the message
to the existing scopeComm logging category using the appropriate
qCDebug/qCInfo/qCWarning call, so adapter-side log messages flow through
ScopeLogging into DiagnosticModel alongside host-side entries.

Tests updated: notificationIgnored now also verifies diagnosticReceived
stays silent for non-diagnostic notifications. Two new tests cover
warning and debug level forwarding.

https://claude.ai/code/session_01UhRoTiKzKdkuurvzKQBVjr
Guard against malformed adapter.diagnostic params in
onNotificationReceived: if params is not a JSON object the signal is not
emitted and a warning is logged instead of silently forwarding empty
strings.

Add explicit "warning" branch to onAdapterDiagnostic and an else branch
that logs the unknown level so unrecognised values are visible rather
than being silently promoted.

New test diagnosticMalformedParams verifies no signal is emitted for
non-object params. New tst_modbuspoll covers all four branches of the
level-mapping logic (debug, info, warning, unknown) using a captured
Qt message handler.

https://claude.ai/code/session_01UhRoTiKzKdkuurvzKQBVjr
@jgeudens jgeudens force-pushed the claude/route-logging-diagnostics-4Awjy branch from c91f778 to 602a676 Compare April 3, 2026 18:12
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/ProtocolAdapter/adapterclient.cpp (1)

176-178: ⚠️ Potential issue | 🟡 Minor

Validate level and message types before emitting diagnostics.

At the moment, missing/non-string fields silently become empty strings via toString(), which can create misleading downstream logs.

Proposed fix
-    QJsonObject obj = params.toObject();
-    emit diagnosticReceived(obj.value(QStringLiteral("level")).toString(),
-                            obj.value(QStringLiteral("message")).toString());
+    const QJsonObject obj = params.toObject();
+    const QJsonValue levelValue = obj.value(QStringLiteral("level"));
+    const QJsonValue messageValue = obj.value(QStringLiteral("message"));
+    if (!levelValue.isString() || !messageValue.isString())
+    {
+        qCWarning(scopeComm) << "AdapterClient: adapter.diagnostic requires string level and message";
+        return;
+    }
+
+    emit diagnosticReceived(levelValue.toString(), messageValue.toString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 176 - 178, The code
currently reads `params.toObject()` and blindly calls `toString()` on
`obj.value("level")` and `obj.value("message")`; validate that both keys exist
and that `obj.value(QStringLiteral("level")).isString()` and
`obj.value(QStringLiteral("message")).isString()` before converting to QString
and calling `emit diagnosticReceived(... )`; if either check fails, avoid
emitting a potentially misleading empty/invalid diagnostic (log or emit an
explicit malformed-diagnostic warning instead). Ensure you update the logic
around the `QJsonObject obj = params.toObject();` block and the `emit
diagnosticReceived` call so the values are type-checked first and only converted
when valid.
🧹 Nitpick comments (1)
tests/communication/tst_modbuspoll.cpp (1)

72-82: Add a dedicated test for the protocol-defined "error" level.

The suite currently validates an unknown "critical" level path, but it does not assert behaviour for "error", which is now part of the protocol contract.

Based on learnings: Applies to tests/**/*.cpp : When fixing a bug, add a test that reproduces the issue before implementing the fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/communication/tst_modbuspoll.cpp` around lines 72 - 82, Add a new unit
test mirroring TestModbusPoll::diagnosticUnknownLevel that invokes the same
onAdapterDiagnostic method on _pModbusPoll but passes QStringLiteral("error") as
the diagnostic level and an appropriate message, installs captureHandler via
qInstallMessageHandler, restores the previous handler, and then asserts the
expected behavior (e.g., QCOMPARE on g_capturedType and QVERIFY on
g_capturedMessage) to validate the protocol-defined "error" level path;
reference TestModbusPoll::diagnosticUnknownLevel and the onAdapterDiagnostic
invocation to implement the new test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@adapters/describe.json`:
- Line 15: The defaults in adapters/describe.json for the fields persistent and
int32LittleEndian were changed to true while adapters/json-rpc-spec.md still
documents them as false, causing an API mismatch for adapter.describe; fix by
making them consistent: either revert the two occurrences of persistent and
int32LittleEndian in adapters/describe.json back to false to match the spec, or
update adapters/json-rpc-spec.md to state the new true defaults—ensure you
update both places where those keys appear in adapters/describe.json and update
the adapter.describe section of the spec accordingly so runtime behavior and
documentation match.

In `@src/communication/modbuspoll.cpp`:
- Around line 123-149: The onAdapterDiagnostic function currently misses
handling the protocol "error" level causing those messages to hit the fallback;
add an else-if branch that checks for level == QStringLiteral("error") and route
it to qCCritical(scopeAdapter) << message (use qCCritical to preserve proper
severity), leaving other branches and the fallback unchanged so unknown levels
still warn; update only the ModbusPoll::onAdapterDiagnostic function and
reference the scopeAdapter logging category when adding the mapping.

In `@src/ProtocolAdapter/adapterclient.h`:
- Around line 117-120: Update the docstring for the signal that is emitted on
adapter.diagnostic notifications (the comment block above the signal declaration
in adapterclient.h) to include the missing "error" severity level; change the
severity list from "debug", "info", or "warning" to include "error" (e.g.,
"debug", "info", "warning", or "error") so the documentation matches the current
protocol.

In `@tests/communication/tst_modbuspoll.cpp`:
- Around line 36-82: Before invoking onAdapterDiagnostic in each TestModbusPoll
test (diagnosticDebugLevel, diagnosticInfoLevel, diagnosticWarningLevel,
diagnosticUnknownLevel), reset the shared capture state g_capturedType and
g_capturedMessage to their default/empty values so prior test output cannot mask
failures; do this immediately before qInstallMessageHandler(captureHandler)
(references: g_capturedType, g_capturedMessage, captureHandler, _pModbusPoll,
onAdapterDiagnostic).
- Line 26: The test enables the wrong logging category so adapter debug messages
never reach the handler; update the QLoggingCategory::setFilterRules call used
in the test setup to include the adapter category (e.g., enable
scope.comm.adapter.debug=true or a wildcard like scope.comm.*.debug=true) so the
messages emitted by onAdapterDiagnostic (category scope.comm.adapter) are
delivered and the debug-level assertions become reliable.

---

Duplicate comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 176-178: The code currently reads `params.toObject()` and blindly
calls `toString()` on `obj.value("level")` and `obj.value("message")`; validate
that both keys exist and that `obj.value(QStringLiteral("level")).isString()`
and `obj.value(QStringLiteral("message")).isString()` before converting to
QString and calling `emit diagnosticReceived(... )`; if either check fails,
avoid emitting a potentially misleading empty/invalid diagnostic (log or emit an
explicit malformed-diagnostic warning instead). Ensure you update the logic
around the `QJsonObject obj = params.toObject();` block and the `emit
diagnosticReceived` call so the values are type-checked first and only converted
when valid.

---

Nitpick comments:
In `@tests/communication/tst_modbuspoll.cpp`:
- Around line 72-82: Add a new unit test mirroring
TestModbusPoll::diagnosticUnknownLevel that invokes the same onAdapterDiagnostic
method on _pModbusPoll but passes QStringLiteral("error") as the diagnostic
level and an appropriate message, installs captureHandler via
qInstallMessageHandler, restores the previous handler, and then asserts the
expected behavior (e.g., QCOMPARE on g_capturedType and QVERIFY on
g_capturedMessage) to validate the protocol-defined "error" level path;
reference TestModbusPoll::diagnosticUnknownLevel and the onAdapterDiagnostic
invocation to implement the new test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a674f537-a009-4533-9b9f-3863c3db0596

📥 Commits

Reviewing files that changed from the base of the PR and between c91f778 and 602a676.

⛔ Files ignored due to path filters (2)
  • adapters/dummymodbusadapter.exe is excluded by !**/*.exe
  • adapters/modbusadapter.exe is excluded by !**/*.exe
📒 Files selected for processing (16)
  • adapters/describe.json
  • adapters/dummymodbusadapter
  • adapters/json-rpc-spec.md
  • adapters/modbusadapter
  • src/ProtocolAdapter/adapterclient.cpp
  • src/ProtocolAdapter/adapterclient.h
  • src/communication/modbuspoll.cpp
  • src/communication/modbuspoll.h
  • src/util/scopelogging.cpp
  • src/util/scopelogging.h
  • tests/CMakeLists.txt
  • tests/ProtocolAdapter/tst_adapterclient.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h
  • tests/communication/CMakeLists.txt
  • tests/communication/tst_modbuspoll.cpp
  • tests/communication/tst_modbuspoll.h
✅ Files skipped from review due to trivial changes (3)
  • tests/CMakeLists.txt
  • src/util/scopelogging.cpp
  • tests/communication/tst_modbuspoll.h
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/communication/CMakeLists.txt
  • src/communication/modbuspoll.h
  • tests/ProtocolAdapter/tst_adapterclient.h
  • tests/ProtocolAdapter/tst_adapterclient.cpp

"id": 0,
"ip": "127.0.0.1",
"persistent": false,
"persistent": true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Defaults now diverge from the published contract.

persistent and int32LittleEndian were changed to true, but adapters/json-rpc-spec.md still documents both defaults as false. This creates a behavioural/API mismatch for clients consuming adapter.describe.

Proposed fix (if the default change was unintentional)
-                "persistent": true,
+                "persistent": false,
...
-                "int32LittleEndian": true,
+                "int32LittleEndian": false,

Also applies to: 26-26

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@adapters/describe.json` at line 15, The defaults in adapters/describe.json
for the fields persistent and int32LittleEndian were changed to true while
adapters/json-rpc-spec.md still documents them as false, causing an API mismatch
for adapter.describe; fix by making them consistent: either revert the two
occurrences of persistent and int32LittleEndian in adapters/describe.json back
to false to match the spec, or update adapters/json-rpc-spec.md to state the new
true defaults—ensure you update both places where those keys appear in
adapters/describe.json and update the adapter.describe section of the spec
accordingly so runtime behavior and documentation match.

Comment on lines +117 to +120
/*!
* \brief Emitted when an adapter.diagnostic notification is received from the adapter.
* \param level Severity level string: "debug", "info", or "warning".
* \param message The diagnostic message from the adapter.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Signal docs are stale for diagnostic severity levels.

The comment lists only "debug", "info", and "warning", but the protocol now includes "error". Please update this docstring to avoid misleading consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.h` around lines 117 - 120, Update the
docstring for the signal that is emitted on adapter.diagnostic notifications
(the comment block above the signal declaration in adapterclient.h) to include
the missing "error" severity level; change the severity list from "debug",
"info", or "warning" to include "error" (e.g., "debug", "info", "warning", or
"error") so the documentation matches the current protocol.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 117-119: Do not set _state = State::IDLE immediately after calling
_pProcess->stop(); instead leave the state as STOPPING/ERROR until the child
process actually exits, and only transition to State::IDLE inside
onProcessFinished() after processFinished() is observed; update the three
locations that currently clear _state (the Unexpected non-object path emitting
sessionError, the timeout path that emits sessionStopped(), and the other
error/stop branches referenced) to call _pProcess->stop() without resetting
_state, ensure timers/signals are not prematurely stopped so onProcessFinished()
still receives the completion event, and adjust prepareAdapter()/State::STOPPING
handling so fast retries cannot start a new process while the old process is
still running.

In `@src/ProtocolAdapter/adapterprocess.cpp`:
- Around line 51-59: Replace the QTimer::singleShot lambda with a cancellable
timer and a named handler: add an int member (e.g., m_stopTimerId = 0) and use
startTimer(cStopTimeoutMs) instead of QTimer::singleShot to store the returned
timer id; implement a private handler (e.g., void handleStopTimeout() or
override timerEvent and dispatch when timerId == m_stopTimerId) that checks
_pProcess->state() != QProcess::NotRunning and calls _pProcess->kill(); ensure
you call killTimer(m_stopTimerId) and reset m_stopTimerId to 0 from start() and
onProcessFinished() to cancel any pending stop timeout before restarting or
after exit, and remove the lambda to satisfy the "no multi-statement lambda"
guideline.

In `@tests/ProtocolAdapter/tst_adapterprocess.cpp`:
- Around line 47-50: Replace the fixed QTest::qWait(500) sleeps in
tst_adapterprocess.cpp with signal-driven waits: after calling
AdapterProcess::stop() use a QSignalSpy that watches the process finished signal
(the same signal used in processFinishedEmittedOnStop()) and call
spy.wait(cStopTimeoutMs) so the test waits up to cStopTimeoutMs (3000 ms)
instead of a fixed 500ms; similarly, in sendRequestEmitsResponseReceived()
replace the fixed qWait with a QSignalSpy on the response-received signal used
by the test and call spy.wait(cStopTimeoutMs). Ensure you reference the same
signal names the tests currently expect and remove the QTest::qWait calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8fdba55-d8fc-4f30-beca-ec2ac7cbdc41

📥 Commits

Reviewing files that changed from the base of the PR and between 602a676 and 3dfc900.

📒 Files selected for processing (12)
  • src/ProtocolAdapter/adapterclient.cpp
  • src/ProtocolAdapter/adapterclient.h
  • src/ProtocolAdapter/adapterprocess.cpp
  • src/ProtocolAdapter/adapterprocess.h
  • src/communication/modbuspoll.cpp
  • src/models/diagnosticmodel.cpp
  • tests/ProtocolAdapter/tst_adapterclient.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h
  • tests/ProtocolAdapter/tst_adapterprocess.cpp
  • tests/communication/tst_modbuspoll.cpp
  • tests/models/tst_diagnosticmodel.cpp
  • tests/models/tst_diagnosticmodel.h
✅ Files skipped from review due to trivial changes (1)
  • src/ProtocolAdapter/adapterprocess.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/communication/modbuspoll.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h

Comment on lines 117 to 119
_state = State::IDLE;
QTimer::singleShot(0, this, [this]() { _pProcess->stop(); });
_pProcess->stop();
emit sessionError(QString("Unexpected non-object result for %1").arg(method));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not switch back to IDLE until the child has actually exited.

AdapterProcess::stop() now returns immediately, but these paths still clear _state early and the timeout branch even emits sessionStopped() before processFinished(). A fast retry can then re-enter prepareAdapter() while the old process is still alive, and the State::STOPPING error path drops its completion signal because the timer is stopped and onProcessFinished() sees IDLE. Keep a non-idle stopping/error state until the process exit is observed, then transition to IDLE.

Also applies to: 130-132, 147-175

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 117 - 119, Do not set
_state = State::IDLE immediately after calling _pProcess->stop(); instead leave
the state as STOPPING/ERROR until the child process actually exits, and only
transition to State::IDLE inside onProcessFinished() after processFinished() is
observed; update the three locations that currently clear _state (the Unexpected
non-object path emitting sessionError, the timeout path that emits
sessionStopped(), and the other error/stop branches referenced) to call
_pProcess->stop() without resetting _state, ensure timers/signals are not
prematurely stopped so onProcessFinished() still receives the completion event,
and adjust prepareAdapter()/State::STOPPING handling so fast retries cannot
start a new process while the old process is still running.

Comment on lines +51 to +59
if (_pProcess->state() != QProcess::NotRunning)
{
_pProcess->closeWriteChannel();
if (!_pProcess->waitForFinished(3000))
{
_pProcess->kill();
_pProcess->waitForFinished(1000);
}
QTimer::singleShot(cStopTimeoutMs, this, [this]() {
if (_pProcess->state() != QProcess::NotRunning)
{
_pProcess->kill();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/ProtocolAdapter && wc -l adapterprocess.cpp adapterprocess.h

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 130


🏁 Script executed:

cd src/ProtocolAdapter && head -80 adapterprocess.cpp | tail -40

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 1138


🏁 Script executed:

cd src/ProtocolAdapter && grep -n "void.*start\|void.*stop\|void.*onProcessFinished" adapterprocess.cpp | head -20

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 194


🏁 Script executed:

cd src/ProtocolAdapter && cat adapterprocess.h

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 3744


🏁 Script executed:

cd src/ProtocolAdapter && sed -n '20,50p' adapterprocess.cpp

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 1035


🏁 Script executed:

cd src/ProtocolAdapter && sed -n '172,195p' adapterprocess.cpp

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 332


🏁 Script executed:

cd src/ProtocolAdapter && grep -n "QTimer\|_pTimer\|killTimer\|stopTimer" adapterprocess.cpp adapterprocess.h

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 194


Cancel any pending stop timeout before a restart.

The singleShot timer is not cancelled when start() or onProcessFinished() is called. If the child process exits quickly and a new adapter is started within cStopTimeoutMs, the stale callback will fire and kill the fresh process because _pProcess is running again. Store the timer ID returned by singleShot as a member and call killTimer() from start() and onProcessFinished().

Additionally, the lambda contains multiple statements and violates the coding guideline: "Avoid lambda expressions with more than 2 captures or multiple statements; use named functions instead for clarity". Refactor this into a named private slot or function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterprocess.cpp` around lines 51 - 59, Replace the
QTimer::singleShot lambda with a cancellable timer and a named handler: add an
int member (e.g., m_stopTimerId = 0) and use startTimer(cStopTimeoutMs) instead
of QTimer::singleShot to store the returned timer id; implement a private
handler (e.g., void handleStopTimeout() or override timerEvent and dispatch when
timerId == m_stopTimerId) that checks _pProcess->state() != QProcess::NotRunning
and calls _pProcess->kill(); ensure you call killTimer(m_stopTimerId) and reset
m_stopTimerId to 0 from start() and onProcessFinished() to cancel any pending
stop timeout before restarting or after exit, and remove the lambda to satisfy
the "no multi-statement lambda" guideline.

Comment on lines 47 to +50
process.stop();

/* stop() is non-blocking; give the event loop time to receive the response */
QTest::qWait(500);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "tst_adapterprocess.cpp" -o -name "adapterprocess.cpp" -o -name "adapterprocess.h" | head -20

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 200


🏁 Script executed:

cat -n tests/ProtocolAdapter/tst_adapterprocess.cpp | sed -n '40,85p'

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 1690


🏁 Script executed:

cat -n src/ProtocolAdapter/adapterprocess.cpp | grep -A 10 -B 5 "cStopTimeoutMs\|stop()"

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 1611


🏁 Script executed:

cat -n src/ProtocolAdapter/adapterprocess.h | grep -A 15 "stop()"

Repository: ModbusScope/NotOnlyModbusScope

Length of output: 673


These fixed 500 ms waits are shorter than the stop contract and introduce race conditions.

AdapterProcess::stop() may legitimately take up to 3000 ms before the forced-kill path runs (line 54 in adapterprocess.cpp). Both sleeps at lines 50 and 73 are insufficient and timing-dependent. processFinishedEmittedOnStop() can fail on slower systems during valid graceful shutdown, and sendRequestEmitsResponseReceived() relies on fixed delay rather than signal arrival. Replace these with signal-driven waits: use QSignalSpy::wait() with timeout matching cStopTimeoutMs (3000 ms) instead of QTest::qWait(500).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/ProtocolAdapter/tst_adapterprocess.cpp` around lines 47 - 50, Replace
the fixed QTest::qWait(500) sleeps in tst_adapterprocess.cpp with signal-driven
waits: after calling AdapterProcess::stop() use a QSignalSpy that watches the
process finished signal (the same signal used in processFinishedEmittedOnStop())
and call spy.wait(cStopTimeoutMs) so the test waits up to cStopTimeoutMs (3000
ms) instead of a fixed 500ms; similarly, in sendRequestEmitsResponseReceived()
replace the fixed qWait with a QSignalSpy on the response-received signal used
by the test and call spy.wait(cStopTimeoutMs). Ensure you reference the same
signal names the tests currently expect and remove the QTest::qWait calls.

- Replace fire-and-forget QTimer::singleShot in AdapterProcess::stop()
  with a cancellable QTimer that is stopped on start, stop, and process
  exit, preventing the kill timer from killing a newly started adapter.
- Suppress sessionError emission from onProcessError during STOPPING
  state to avoid spurious error signals during intentional shutdown.
- Disconnect sessionStopped from initAdapter on sessionError in
  ModbusPoll to prevent auto-restart after an unexpected crash.
- Add regression tests for processError during STOPPING state.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 165-179: In onHandshakeTimeout(), don't emit sessionStopped
immediately after calling _pProcess->stop() because stop() is non-blocking;
instead set _state = State::STOPPING (or set a new bool like _handshakeTimedOut
= true) before calling _pProcess->stop(), call _pProcess->stop(), and remove the
immediate emit; then update onProcessFinished() to check the new flag/_state and
emit sessionStopped only once the process has actually finished, and if the
handshake timed out but an error condition should be reported emit sessionError
there instead (use symbols onHandshakeTimeout, _pProcess->stop(), _state,
_handshakeTimedOut (or State::STOPPING), onProcessFinished, sessionStopped, and
sessionError).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f39f3c52-d149-482f-8666-c571f07a97f4

📥 Commits

Reviewing files that changed from the base of the PR and between 3dfc900 and 5e89b95.

📒 Files selected for processing (6)
  • src/ProtocolAdapter/adapterclient.cpp
  • src/ProtocolAdapter/adapterprocess.cpp
  • src/ProtocolAdapter/adapterprocess.h
  • src/communication/modbuspoll.cpp
  • tests/ProtocolAdapter/tst_adapterclient.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ProtocolAdapter/adapterprocess.h
  • src/ProtocolAdapter/adapterprocess.cpp
  • tests/ProtocolAdapter/tst_adapterclient.h

Comment on lines 165 to +179
void AdapterClient::onHandshakeTimeout()
{
qCWarning(scopeComm) << "AdapterClient: handshake timed out in state" << static_cast<int>(_state);
bool wasStopping = (_state == State::STOPPING);
_state = State::IDLE;
QTimer::singleShot(0, this, [this]() { _pProcess->stop(); });
emit sessionError("Adapter handshake timed out");
_pProcess->stop();
if (wasStopping)
{
emit sessionStopped();
}
else
{
emit sessionError("Adapter handshake timed out");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

sessionStopped emitted before process has actually exited in timeout path.

When the shutdown timer fires (line 173), sessionStopped is emitted immediately after calling _pProcess->stop(), but before onProcessFinished() is invoked. Since stop() is now non-blocking, this violates the invariant that sessionStopped should only be emitted once the process has truly exited.

Additionally, when onProcessFinished() later fires, _state will already be IDLE, so it will correctly do nothing—but the caller may have already acted on the premature sessionStopped.

Consider keeping _state = State::STOPPING and letting onProcessFinished() emit sessionStopped:

Proposed fix
 void AdapterClient::onHandshakeTimeout()
 {
     qCWarning(scopeComm) << "AdapterClient: handshake timed out in state" << static_cast<int>(_state);
-    bool wasStopping = (_state == State::STOPPING);
-    _state = State::IDLE;
+    if (_state == State::STOPPING)
+    {
+        // Let onProcessFinished emit sessionStopped after the process exits
+        _pProcess->stop();
+        return;
+    }
+    _state = State::STOPPING;
     _pProcess->stop();
-    if (wasStopping)
-    {
-        emit sessionStopped();
-    }
-    else
-    {
-        emit sessionError("Adapter handshake timed out");
-    }
+    // sessionError will not be emitted since we're now STOPPING;
+    // onProcessFinished will emit sessionStopped
 }

Actually, this changes semantics—errors during handshake should still emit sessionError. A cleaner approach may be to introduce an intermediate state or track "timed-out-during-handshake" separately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 165 - 179, In
onHandshakeTimeout(), don't emit sessionStopped immediately after calling
_pProcess->stop() because stop() is non-blocking; instead set _state =
State::STOPPING (or set a new bool like _handshakeTimedOut = true) before
calling _pProcess->stop(), call _pProcess->stop(), and remove the immediate
emit; then update onProcessFinished() to check the new flag/_state and emit
sessionStopped only once the process has actually finished, and if the handshake
timed out but an error condition should be reported emit sessionError there
instead (use symbols onHandshakeTimeout, _pProcess->stop(), _state,
_handshakeTimedOut (or State::STOPPING), onProcessFinished, sessionStopped, and
sessionError).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/ProtocolAdapter/adapterclient.cpp (2)

117-120: ⚠️ Potential issue | 🟠 Major

Do not transition to State::IDLE before process exit is observed.

At Line [117], Line [133], and Line [173], _state is set to IDLE before _pProcess->stop() completes; Line [177] also emits sessionStopped immediately in the timeout/STOPPING path. Since stop() is non-blocking, this can allow fast re-entry while the old process is still alive and can suppress terminal signalling in onProcessFinished(). Keep a non-idle terminal state until processFinished arrives, and emit sessionStopped only from onProcessFinished().

Also applies to: 133-136, 172-182

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 117 - 120, The code sets
_state = State::IDLE before calling _pProcess->stop() and even emits
sessionStopped in the timeout/STOPPING path, which allows re-entry while the old
process is still alive; change the logic so you do NOT set _state to State::IDLE
or emit sessionStopped before the process exit is observed: call
_pProcess->stop() without changing _state (leave it in STOPPING or a dedicated
terminal state), remove any premature sessionStopped emission in the
timeout/STOPPING path, and ensure that the only place that sets _state =
State::IDLE and emits sessionStopped is onProcessFinished() after confirming the
process has exited.

198-200: ⚠️ Potential issue | 🟡 Minor

Validate adapter.diagnostic field types before emitting.

At Line [199]-Line [200], missing/non-string level or message is silently converted to empty strings, which can produce misleading diagnostics downstream. Guard both fields with isString() and drop invalid payloads with a warning.

Proposed fix
-    QJsonObject obj = params.toObject();
-    emit diagnosticReceived(obj.value(QStringLiteral("level")).toString(),
-                            obj.value(QStringLiteral("message")).toString());
+    const QJsonObject obj = params.toObject();
+    const QJsonValue levelValue = obj.value(QStringLiteral("level"));
+    const QJsonValue messageValue = obj.value(QStringLiteral("message"));
+    if (!levelValue.isString() || !messageValue.isString())
+    {
+        qCWarning(scopeComm) << "AdapterClient: adapter.diagnostic requires string level and message, params="
+                             << params;
+        return;
+    }
+
+    emit diagnosticReceived(levelValue.toString(), messageValue.toString());
#!/bin/bash
# Verify whether regression coverage exists for malformed adapter.diagnostic payloads.
fd -i 'tst_adapterclient.cpp' | while read -r file; do
  echo "== $file =="
  rg -nP -C2 'adapter\.diagnostic|diagnosticReceived|level|message|isString' "$file"
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 198 - 200, The code
currently reads level/message from params without type checks and silently
converts non-strings to empty values before calling diagnosticReceived; update
the handler around QJsonObject obj = params.toObject() to validate that
obj.value(QStringLiteral("level")) and obj.value(QStringLiteral("message")) both
.isString() before calling emit diagnosticReceived(...), and if either check
fails, do not emit diagnosticReceived and instead log a warning (e.g.,
qWarning()) that includes the invalid payload (params) so malformed
adapter.diagnostic messages are dropped and visible for debugging.
🧹 Nitpick comments (1)
src/ProtocolAdapter/adapterclient.cpp (1)

7-8: Add source-level Doxygen for the updated public constructor.

AdapterClient::AdapterClient(...) was updated but still lacks a brief Doxygen block in the .cpp source.

As per coding guidelines: **/*.{cpp,cxx}: Document all public functions with brief Doxygen comments in the source file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ProtocolAdapter/adapterclient.cpp` around lines 7 - 8, Add a short
Doxygen block immediately above the AdapterClient::AdapterClient(AdapterProcess*
pProcess, QObject* parent, int handshakeTimeoutMs) definition in the .cpp file:
include a one-line `@brief` describing the constructor purpose and brief `@param`
tags for pProcess, parent and handshakeTimeoutMs (e.g., handshake timeout in
milliseconds), and, if relevant, an `@note` about ownership/lifetime of pProcess;
place it directly above the constructor signature so the source-level
documentation guideline for public functions is satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 117-120: The code sets _state = State::IDLE before calling
_pProcess->stop() and even emits sessionStopped in the timeout/STOPPING path,
which allows re-entry while the old process is still alive; change the logic so
you do NOT set _state to State::IDLE or emit sessionStopped before the process
exit is observed: call _pProcess->stop() without changing _state (leave it in
STOPPING or a dedicated terminal state), remove any premature sessionStopped
emission in the timeout/STOPPING path, and ensure that the only place that sets
_state = State::IDLE and emits sessionStopped is onProcessFinished() after
confirming the process has exited.
- Around line 198-200: The code currently reads level/message from params
without type checks and silently converts non-strings to empty values before
calling diagnosticReceived; update the handler around QJsonObject obj =
params.toObject() to validate that obj.value(QStringLiteral("level")) and
obj.value(QStringLiteral("message")) both .isString() before calling emit
diagnosticReceived(...), and if either check fails, do not emit
diagnosticReceived and instead log a warning (e.g., qWarning()) that includes
the invalid payload (params) so malformed adapter.diagnostic messages are
dropped and visible for debugging.

---

Nitpick comments:
In `@src/ProtocolAdapter/adapterclient.cpp`:
- Around line 7-8: Add a short Doxygen block immediately above the
AdapterClient::AdapterClient(AdapterProcess* pProcess, QObject* parent, int
handshakeTimeoutMs) definition in the .cpp file: include a one-line `@brief`
describing the constructor purpose and brief `@param` tags for pProcess, parent
and handshakeTimeoutMs (e.g., handshake timeout in milliseconds), and, if
relevant, an `@note` about ownership/lifetime of pProcess; place it directly above
the constructor signature so the source-level documentation guideline for public
functions is satisfied.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4efb517f-24f4-4e07-bb28-0f4aaa046d93

📥 Commits

Reviewing files that changed from the base of the PR and between 5e89b95 and 4b6ba45.

⛔ Files ignored due to path filters (2)
  • adapters/dummymodbusadapter.exe is excluded by !**/*.exe
  • adapters/modbusadapter.exe is excluded by !**/*.exe
📒 Files selected for processing (9)
  • adapters/dummymodbusadapter
  • adapters/modbusadapter
  • src/ProtocolAdapter/adapterclient.cpp
  • src/ProtocolAdapter/adapterclient.h
  • src/communication/modbuspoll.cpp
  • src/communication/modbuspoll.h
  • tests/communication/tst_modbuspoll.cpp
  • tests/communication/tst_modbuspoll.h
  • tests/models/tst_diagnosticmodel.h
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/communication/modbuspoll.h
  • tests/models/tst_diagnosticmodel.h
  • src/communication/modbuspoll.cpp
  • src/ProtocolAdapter/adapterclient.h

@jgeudens jgeudens merged commit ecf857b into master Apr 5, 2026
10 checks passed
@jgeudens jgeudens deleted the claude/route-logging-diagnostics-4Awjy branch April 5, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants